/*
 * File     : FastCharsets.java
 * Created  : 5 May 2011
 *
 * Copyright © 2011 Matthew Wilson (mj. {my-surname} .uk {at} gmail.com)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.googlecode.dni.internal.string;

import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import com.googlecode.dni.Library;
import com.googlecode.dni.internal.NativeUnsafeAccess;

/**
 * Provides a service to find a faster string encoding / decoding
 * implementation.
 *
 * @author Matthew Wilson
 */
public class FastCharset
{

    private static final Map< String, FastCharset > INSTANCES = new HashMap< String, FastCharset >();

    static
    {
        initFast( new AsciiCoder.AsciiFastCharset() );
        initFast( new Iso8859_1Coder.Iso8859_1FastCharset() );
        initFast( new Utf16Coder.Utf16FastCharset() );
        initFast( new Utf8Coder.Utf8FastCharset() );
        initFast( new Windows1252Coder.Windows1252FastCharset() );

        INSTANCES.put( Library.DEFAULT_CHARSET,
                       getInstance( Charset.defaultCharset().name() ) );
        INSTANCES.put( "",
                       getInstance( Charset.defaultCharset().name() ) );
    }


    private final String staticClassName;

    private final String name;

    private final Charset charset;

    private final float maxBytesPerChar;

    /**
     * @param name
     *            the charset name
     * @param staticClassName
     *            the static class name, if any
     * @param charset
     *            the charset
     */
    FastCharset( final String name,
                 final String staticClassName,
                 final Charset charset )
    {
        this.name = name;
        this.staticClassName = staticClassName;
        this.charset = charset;
        this.maxBytesPerChar = charset != null ? charset.newEncoder().maxBytesPerChar() : Float.NaN;
    }

    /**
     * @return the name of the charset
     */
    public String getName()
    {
        return this.name;
    }

    /**
     * Obtains the name of a static class that has methods to encode quickly.
     * If none is present, <code>null</code> is returned.
     *
     * @return the static class name, or <code>null</code>
     */
    public String getStaticClassName()
    {
        return this.staticClassName;
    }

    /**
     * Obtains an upper bound on the space required for the given string with a
     * null terminator.
     *
     * @param string
     *            the string
     * @return the size in bytes
     */
    public int getByteCount( final String string )
    {
        return (int) ( this.maxBytesPerChar * string.length() + 2 );
    }

    /**
     * Encodes the given string into the given buffer.  The position is then
     * undefined.
     *
     * @param string
     *            the string
     * @param buffer
     *            the buffer
     * 
     * @return the number of bytes used (including terminator)
     */
    public int encode( final String string,
                       final ByteBuffer buffer )
    {
        int start = buffer.position();
        CharsetEncoder encoder = this.charset.newEncoder();
        encoder.onMalformedInput( CodingErrorAction.REPLACE ).onUnmappableCharacter( CodingErrorAction.REPLACE );
        encoder.encode( CharBuffer.wrap( string ), buffer, true );
        buffer.put( (byte) 0 );
        return buffer.position() - start;
    }

    /**
     * Reads a null-terminated string.
     *
     * @param pointer
     *            the start of the string
     *
     * @return a string
     */
    public String decode( final long pointer )
    {
        if ( pointer == 0 )
        {
            return null;
        }

        // TODO consider if this can be sped up

        CharsetDecoder decoder = this.charset.newDecoder();
        decoder.onMalformedInput( CodingErrorAction.REPLACE ).onUnmappableCharacter( CodingErrorAction.REPLACE );

        ByteBuffer buffer = NativeUnsafeAccess.getBuffer( pointer, 1048576 );
        CharBuffer charBuffer = CharBuffer.allocate( 4096 );
        while ( true )
        {
            buffer.limit( buffer.position() + 1 );
            CoderResult result = decoder.decode( buffer, charBuffer, false );
            if ( result.isOverflow() )
            {
                CharBuffer newCharBuffer = CharBuffer.allocate( charBuffer.capacity() * 2 );
                charBuffer.flip();
                newCharBuffer.put( charBuffer );
                charBuffer = newCharBuffer;
            }
            else
            {
                if ( charBuffer.position() > 0 && charBuffer.get( charBuffer.position() - 1 ) == 0 )
                {
                    // TODO assuming that it's null character (not byte) terminated
                    break;
                }
            }
        }

        return charBuffer.flip().toString();
    }

    /**
     * Obtains an instance for the given charset name.
     *
     * @param name
     *            the charset name
     *
     * @return an instance
     */
    public static FastCharset getInstance( final String name )
    {
        synchronized ( INSTANCES )
        {
            String nameLC = name.toLowerCase( Locale.US );
            FastCharset instance = INSTANCES.get( nameLC );
            if ( instance == null )
            {
                instance = new FastCharset( name, null, Charset.forName( name ) );
            }
            return instance;
        }
    }

    private static void initFast( final FastCharset fastCharset )
    {
        INSTANCES.put( fastCharset.name.toLowerCase( Locale.US ), fastCharset );
    }

    /** Nasty utility class to build lookup tables. */
    static class Builder
    {
        /**
         * @param args command-line arguments
         * @throws CharacterCodingException on failure
         */
        public static void main( final String[] args ) throws CharacterCodingException
        {
            for ( String arg : args )
            {
                Charset charset = Charset.forName( arg );
                CharsetEncoder encoder = charset.newEncoder();
                encoder.onMalformedInput( CodingErrorAction.REPLACE ).onUnmappableCharacter( CodingErrorAction.REPLACE );
                encoder.replaceWith( new byte[] { '?' } );


                byte[] table = new byte[ 65536 ];
                char[] reverse = new char[ 256 ];
                Arrays.fill( reverse, '\0' );

                for ( int i = 0; i < 65536; i++ )
                {
                    if ( i >= 0xd800 && i <= 0xdfff )
                    {
                        table[ i ] = '?';
                    }
                    else
                    {
                        ByteBuffer encode = encoder.encode( CharBuffer.wrap( new char[] { (char) i } ) );
                        if ( encode.remaining() != 1 )
                        {
                            throw new AssertionError();
                        }
                        byte b = encode.get( 0 );
                        table[ i ] = b;
                        if ( encoder.canEncode( (char) i ) )
                        {
                            if ( reverse[ b & 0xff ] != 0 )
                            {
                                throw new AssertionError( "DUPLICATE: " + (char) i + " " + reverse[ b & 0xff ] + " " + b);
                            }
                            reverse[ b & 0xff ] = (char) i;
                        }
                    }
                }

                int end;
                for ( end = 65535; end >= 0; end-- )
                {
                    if ( table[ end ] != '?' )
                    {
                        break;
                    }
                }

                System.out.println( "// Charset: " + arg + " [" + end + " entries]" );
                String symName = arg.replace( '-', '_' ).toLowerCase();
                System.out.println( "JNIEXPORT const int dni_charset_forward_table_length_" + symName + ( end + 1 ) );
                System.out.println();
                System.out.println( "JNIEXPORT const char* dni_charset_forward_table_" + arg.replace( '-', '_' ) + " =" );

                int x = 0;
                for ( int i = 0; i <= end; i++ )
                {
                    if ( x == 0 )
                    {
                        System.out.print( "    \"" );
                    }
                    x++;

                    byte b = table[ i ];
                    if ( b >= 32 && b <= 126 && b != '\\' && b != '\"' && b != '\'' )
                    {
                        System.out.print( (char) b );
                    }
                    else
                    {
                        System.out.printf( "\\%03o", b & 0xff );
                        x+=3;
                    }

                    if ( x > 63 || i == end )
                    {
                        x = 0;
                        if ( i == end )
                        {
                            System.out.println( "\";" );
                        }
                        else
                        {
                            System.out.println( "\"" );
                        }
                    }
                }
                System.out.println();

                System.out.println( "JNIEXPORT const jchar dni_charset_reverse_table_" + arg.replace( '-', '_' ) + "[ 256 ] =" );
                System.out.println( "{" );

                x = 0;
                for ( int i = 0; i <= 0xff; i++ )
                {
                    if ( x == 0 )
                    {
                        System.out.print( "   " );
                    }
                    x++;

                    char b = reverse[ i ];

                    System.out.print( " " );
                    System.out.printf( "0x%04x", (int) b );
                    System.out.print( "," );


                    x++;
                    if ( x > 15 || i == 0xff )
                    {
                        x = 0;
                        System.out.println();
                    }
                }
                System.out.println( "};" );
                System.out.println();
            }
        }
    }

}
